/*
 * @(#)ResourceCatalog.java 1.6 05/11/17 Copyright (c) 2006 Sun Microsystems,
 * Inc. All Rights Reserved. Redistribution and use in source and binary forms,
 * with or without modification, are permitted provided that the following
 * conditions are met: -Redistribution of source code must retain the above
 * copyright notice, this list of conditions and the following disclaimer.
 * -Redistribution in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution. Neither the name of
 * Sun Microsystems, Inc. or the names of contributors may be used to endorse or
 * promote products derived from this software without specific prior written
 * permission. This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES,
 * INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
 * PURPOSE OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC.
 * ("SUN") AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY
 * LICENSEE AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. You
 * acknowledge that this software is not designed, licensed or intended for use
 * in the design, construction, operation or maintenance of any nuclear
 * facility.
 */

package jnlp.sample.servlet;

import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.io.File;
import java.io.BufferedInputStream;
import javax.servlet.ServletContext;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.w3c.dom.*;
import jnlp.sample.util.VersionString;
import jnlp.sample.util.VersionID;

public class ResourceCatalog
{
    public static final String VERSION_XML_FILENAME = "version.xml";

    private Logger _log = null;

    private ServletContext _servletContext = null;

    private HashMap _entries;

    /**
     * Class to contain the information we know about a specific directory
     */
    static private class PathEntries
    {
	/* Version-based entries at this particular path */
	private List _versionXmlList;

	private List _directoryList;

	private List _platformList;

	/* Last time this entry was updated */
	private long _lastModified; // Last modified time of entry;

	public PathEntries(List versionXmlList, List directoryList, List platformList,
		long lastModified)
	{
	    _versionXmlList = versionXmlList;
	    _directoryList = directoryList;
	    _platformList = platformList;
	    _lastModified = lastModified;
	}

	public void setDirectoryList(List dirList)
	{
	    _directoryList = dirList;
	}

	public List getVersionXmlList()
	{
	    return _versionXmlList;
	}

	public List getDirectoryList()
	{
	    return _directoryList;
	}

	public List getPlatformList()
	{
	    return _platformList;
	}

	public long getLastModified()
	{
	    return _lastModified;
	}
    }

    public ResourceCatalog(ServletContext servletContext, Logger log)
    {
	_entries = new HashMap();
	_servletContext = servletContext;
	_log = log;
    }

    public JnlpResource lookupResource(DownloadRequest dreq) throws ErrorResponseException
    {
	// Split request up into path and name
	String path = dreq.getPath();
	String name = null;
	String dir = null;
	int idx = path.lastIndexOf('/');
	if (idx == -1)
	{
	    name = path;
	}
	else
	{
	    name = path.substring(idx + 1); // Exclude '/'
	    dir = path.substring(0, idx + 1); // Include '/'
	}

	// Lookup up already parsed entries, and san directory for entries if
	// neccesary
	PathEntries pentries = (PathEntries) _entries.get(dir);
	JnlpResource xmlVersionResPath = new JnlpResource(_servletContext, dir
		+ VERSION_XML_FILENAME);
	if (pentries == null
		|| (xmlVersionResPath.exists() && xmlVersionResPath.getLastModified() > pentries
			.getLastModified()))
	{
	    _log.addInformational("servlet.log.scandir", dir);
	    List dirList = scanDirectory(dir, dreq);
	    // Scan XML file
	    List versionList = new ArrayList();
	    List platformList = new ArrayList();
	    parseVersionXML(versionList, platformList, dir, xmlVersionResPath);
	    pentries = new PathEntries(versionList, dirList, platformList, xmlVersionResPath
		    .getLastModified());
	    _entries.put(dir, pentries);
	}

	// Search for a match
	JnlpResource[] result = new JnlpResource[1];

	if (dreq.isPlatformRequest())
	{
	    int sts = findMatch(pentries.getPlatformList(), name, dreq, result);
	    if (sts != DownloadResponse.STS_00_OK)
	    {
		throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(sts));
	    }
	}
	else
	{
	    // First lookup in versions.xml file
	    int sts1 = findMatch(pentries.getVersionXmlList(), name, dreq, result);
	    if (sts1 != DownloadResponse.STS_00_OK)
	    {
		// Then lookup in directory
		int sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result);
		if (sts2 != DownloadResponse.STS_00_OK)
		{

		    // fix for 4450104
		    // try rescan and see if it helps
		    pentries.setDirectoryList(scanDirectory(dir, dreq));
		    sts2 = findMatch(pentries.getDirectoryList(), name, dreq, result);
		    // try again after rescanning directory
		    if (sts2 != DownloadResponse.STS_00_OK)
		    {
			// Throw the most specific error code
			throw new ErrorResponseException(DownloadResponse.getJnlpErrorResponse(Math
				.max(sts1, sts2)));
		    }
		}
	    }
	}
	return result[0];
    }

    /**
     * This method finds the best match, or return the best error code. The
     * result parameter must be an array with room for one element. If a match
     * is found, the method returns DownloadResponse.STS_00_OK If one or more
     * entries matches on: name, version-id, os, arch, and locale, then the one
     * with the highest version-id is set in the result[0] field. If a match is
     * not found, it returns an error code, either: ERR_10_NO_RESOURCE,
     * ERR_11_NO_VERSION, ERR_20_UNSUP_OS, ERR_21_UNSUP_ARCH,
     * ERR_22_UNSUP_LOCALE, ERR_23_UNSUP_JRE.
     */
    public int findMatch(List list, String name, DownloadRequest dreq, JnlpResource[] result)
    {
	if (list == null)
	    return DownloadResponse.ERR_10_NO_RESOURCE;
	// Setup return values
	VersionID bestVersionId = null;
	int error = DownloadResponse.ERR_10_NO_RESOURCE;
	VersionString vs = new VersionString(dreq.getVersion());
	// Iterate through entries
	for (int i = 0; i < list.size(); i++)
	{
	    JnlpResource respath = (JnlpResource) list.get(i);
	    VersionID vid = new VersionID(respath.getVersionId());
	    int sts = matchEntry(name, vs, dreq, respath, vid);
	    if (sts == DownloadResponse.STS_00_OK)
	    {
		if (result[0] == null || vid.isGreaterThan(bestVersionId))
		{
		    result[0] = respath;
		    bestVersionId = vid;
		}
	    }
	    else
	    {
		error = Math.max(error, sts);
	    }
	}
	return (result[0] != null) ? DownloadResponse.STS_00_OK : error;
    }

    public int matchEntry(String name, VersionString vs, DownloadRequest dreq,
	    JnlpResource jnlpres, VersionID vid)
    {
	if (!name.equals(jnlpres.getName()))
	{
	    return DownloadResponse.ERR_10_NO_RESOURCE;
	}
	if (!vs.contains(vid))
	{
	    return DownloadResponse.ERR_11_NO_VERSION;
	}
	if (!prefixMatchLists(jnlpres.getOSList(), dreq.getOS()))
	{
	    return DownloadResponse.ERR_20_UNSUP_OS;
	}
	if (!prefixMatchLists(jnlpres.getArchList(), dreq.getArch()))
	{
	    return DownloadResponse.ERR_21_UNSUP_ARCH;
	}
	if (!prefixMatchLists(jnlpres.getLocaleList(), dreq.getLocale()))
	{
	    return DownloadResponse.ERR_22_UNSUP_LOCALE;
	}
	return DownloadResponse.STS_00_OK;
    }

    private static boolean prefixMatchStringList(String[] prefixList, String target)
    {
	// No prefixes matches everything
	if (prefixList == null)
	    return true;
	// No target, but a prefix list does not match anything
	if (target == null)
	    return false;
	for (int i = 0; i < prefixList.length; i++)
	{
	    if (target.startsWith(prefixList[i]))
		return true;
	}
	return false;
    }

    /*
     * Return true if at least one of the strings in 'prefixes' are a prefix to
     * at least one of the 'keys'.
     */
    public boolean prefixMatchLists(String[] prefixes, String[] keys)
    {
	// The prefixes are part of the server resources. If none is given,
	// everything matches
	if (prefixes == null)
	    return true;
	// If no os keyes was given, and the server resource is keyed of this,
	// then return false.
	if (keys == null)
	    return false;
	// Check for a match on a key
	for (int i = 0; i < keys.length; i++)
	{
	    if (prefixMatchStringList(prefixes, keys[i]))
		return true;
	}
	return false;
    }

    /**
     * This method scans the directory pointed to by the given path and creates
     * a list of ResourcePath elements that contains information about all the
     * entries The version-based information is encoded in the file name given
     * the following format: entry ::= <name> __ ( <options> ). <ext> options
     * ::= <option> ( __ <options> )? option ::= V<version-id> | O<os> | A<arch>
     * | L<locale>
     */

    private String jnlpGetPath(DownloadRequest dreq)
    {
	// fix for 4474021
	// try to manuually generate the filename
	// extract file name
	String path = dreq.getPath();
	String filename = path.substring(path.lastIndexOf("/") + 1);
	path = path.substring(0, path.lastIndexOf("/") + 1);
	String name = filename;
	String ext = null;

	if (filename.lastIndexOf(".") != -1)
	{
	    ext = filename.substring(filename.lastIndexOf(".") + 1);

	    filename = filename.substring(0, filename.lastIndexOf("."));

	}
	if (dreq.getVersion() != null)
	{
	    filename += "__V" + dreq.getVersion();
	}

	String[] temp = dreq.getOS();

	if (temp != null)
	{
	    for (int i = 0; i < temp.length; i++)
	    {
		filename += "__O" + temp[i];
	    }
	}

	temp = dreq.getArch();

	if (temp != null)
	{
	    for (int i = 0; i < temp.length; i++)
	    {
		filename += "__A" + temp[i];
	    }
	}
	temp = dreq.getLocale();

	if (temp != null)
	{
	    for (int i = 0; i < temp.length; i++)
	    {
		filename += "__L" + temp[i];
	    }
	}

	if (ext != null)
	{
	    filename += "." + ext;
	}

	path += filename;

	return path;
    }

    public List scanDirectory(String dirPath, DownloadRequest dreq)
    {
	ArrayList list = new ArrayList();

	// fix for 4474021
	if (_servletContext.getRealPath(dirPath) == null)
	{
	    String path = jnlpGetPath(dreq);

	    String name = dreq.getPath().substring(path.lastIndexOf("/") + 1);

	    JnlpResource jnlpres = new JnlpResource(_servletContext, name, dreq.getVersion(), dreq
		    .getOS(), dreq.getArch(), dreq.getLocale(), path, dreq.getVersion());

	    // the file does not exist
	    if (jnlpres.getResource() == null)
		return null;

	    list.add(jnlpres);
	    return list;
	}
	File dir = new File(_servletContext.getRealPath(dirPath));
	_log.addDebug("File directory: " + dir);
	if (dir.exists() && dir.isDirectory())
	{
	    File[] entries = dir.listFiles();
	    for (int i = 0; i < entries.length; i++)
	    {
		JnlpResource jnlpres = parseFileEntry(dirPath, entries[i].getName());
		if (jnlpres != null)
		{
		    if (_log.isDebugLevel())
		    {
			_log.addDebug("Read file resource: " + jnlpres);
		    }
		    list.add(jnlpres);
		}
	    }
	}
	return list;
    }

    private JnlpResource parseFileEntry(String dir, String filename)
    {
	int idx = filename.indexOf("__");
	if (idx == -1)
	    return null;

	// Cut out name
	String name = filename.substring(0, idx);
	String rest = filename.substring(idx);

	// Cut out extension
	idx = rest.lastIndexOf('.');
	String extension = "";
	if (idx != -1)
	{
	    extension = rest.substring(idx);
	    rest = rest.substring(0, idx);
	}

	// Parse options
	String versionId = null;
	ArrayList osList = new ArrayList();
	ArrayList archList = new ArrayList();
	ArrayList localeList = new ArrayList();
	while (rest.length() > 0)
	{
	    /* Must start with __ at this point */
	    if (!rest.startsWith("__"))
		return null;
	    rest = rest.substring(2);
	    // Get option and argument
	    char option = rest.charAt(0);
	    idx = rest.indexOf("__");
	    String arg = null;
	    if (idx == -1)
	    {
		arg = rest.substring(1);
		rest = "";
	    }
	    else
	    {
		arg = rest.substring(1, idx);
		rest = rest.substring(idx);
	    }
	    switch (option)
	    {
	    case 'V':
		versionId = arg;
		break;
	    case 'O':
		osList.add(arg);
		break;
	    case 'A':
		archList.add(arg);
		break;
	    case 'L':
		localeList.add(arg);
		break;
	    default:
		return null; // error
	    }
	}

	return new JnlpResource(_servletContext, name + extension, /*
								    * Resource
								    * name in
								    * URL
								    * request
								    */
	versionId, listToStrings(osList), listToStrings(archList), listToStrings(localeList), dir
		+ filename, /* Resource name in WAR file */
	versionId);
    }

    private String[] listToStrings(List list)
    {
	if (list.size() == 0)
	    return null;
	return (String[]) list.toArray(new String[list.size()]);
    }

    // Returns false if parsing failed
    private void parseVersionXML(final List versionList, final List platformList, final String dir,
	    final JnlpResource versionRes)
    {
	if (!versionRes.exists())
	    return;

	// Parse XML into a more understandable format
	XMLNode root = null;
	try
	{
	    DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
	    DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
	    Document doc = docBuilder.parse(new BufferedInputStream(versionRes.getResource()
		    .openStream()));
	    doc.getDocumentElement().normalize();

	    // Convert document into an XMLNode structure, since we already got
	    // utility methods
	    // to handle these. We should really use the data-binding stuff here
	    // - but that will come
	    // later
	    //
	    root = XMLParsing.convert(doc.getDocumentElement());
	}
	catch (SAXParseException err)
	{
	    _log.addWarning("servlet.log.warning.xml.parsing", versionRes.getPath(), Integer
		    .toString(err.getLineNumber()), err.getMessage());
	    return;
	}
	catch (Throwable t)
	{
	    _log.addWarning("servlet.log.warning.xml.reading", versionRes.getPath(), t);
	    return;
	}

	// Check that root element is a <jnlp> tag
	if (!root.getName().equals("jnlp-versions"))
	{
	    _log.addWarning("servlet.log.warning.xml.missing-jnlp", versionRes.getPath());
	    return;
	}

	// Visit all <resource> elements
	XMLParsing.visitElements(root, "<resource>", new XMLParsing.ElementVisitor()
	{
	    public void visitElement(XMLNode node)
	    {
		XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>");
		if (pattern == null)
		{
		    _log
			    .addWarning("servlet.log.warning.xml.missing-pattern", versionRes
				    .getPath());
		}
		else
		{
		    // Parse pattern
		    String name = XMLParsing.getElementContent(pattern, "<name>", "");
		    String versionId = XMLParsing.getElementContent(pattern, "<version-id>");
		    String[] os = XMLParsing.getMultiElementContent(pattern, "<os>");
		    String[] arch = XMLParsing.getMultiElementContent(pattern, "<arch>");
		    String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>");
		    // Get return request
		    String file = XMLParsing.getElementContent(node, "<file>");
		    if (versionId == null || file == null)
		    {
			_log.addWarning("servlet.log.warning.xml.missing-elems", versionRes
				.getPath());
		    }
		    else
		    {
			JnlpResource res = new JnlpResource(_servletContext, name, versionId, os,
				arch, locale, dir + file, versionId);
			if (res.exists())
			{
			    versionList.add(res);
			    if (_log.isDebugLevel())
			    {
				_log.addDebug("Read resource: " + res);
			    }
			}
			else
			{
			    _log.addWarning("servlet.log.warning.missing-file", file, versionRes
				    .getPath());
			}
		    }
		}
	    }
	});

	// Visit all <resource> elements
	XMLParsing.visitElements(root, "<platform>", new XMLParsing.ElementVisitor()
	{
	    public void visitElement(XMLNode node)
	    {
		XMLNode pattern = XMLParsing.findElementPath(node, "<pattern>");
		if (pattern == null)
		{
		    _log
			    .addWarning("servlet.log.warning.xml.missing-pattern", versionRes
				    .getPath());
		}
		else
		{
		    // Parse pattern
		    String name = XMLParsing.getElementContent(pattern, "<name>", "");
		    String versionId = XMLParsing.getElementContent(pattern, "<version-id>");
		    String[] os = XMLParsing.getMultiElementContent(pattern, "<os>");
		    String[] arch = XMLParsing.getMultiElementContent(pattern, "<arch>");
		    String[] locale = XMLParsing.getMultiElementContent(pattern, "<locale>");
		    // Get return request
		    String file = XMLParsing.getElementContent(node, "<file>");
		    String productId = XMLParsing.getElementContent(node, "<product-version-id>");

		    if (versionId == null || file == null || productId == null)
		    {
			_log.addWarning("servlet.log.warning.xml.missing-elems2", versionRes
				.getPath());
		    }
		    else
		    {
			JnlpResource res = new JnlpResource(_servletContext, name, versionId, os,
				arch, locale, dir + file, productId);
			if (res.exists())
			{
			    platformList.add(res);
			    if (_log.isDebugLevel())
			    {
				_log.addDebug("Read platform resource: " + res);
			    }
			}
			else
			{
			    _log.addWarning("servlet.log.warning.missing-file", file, versionRes
				    .getPath());
			}
		    }
		}
	    }
	});
    }
}
